// Copyright 2017 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <ddk/debug.h>
#include <fbl/auto_lock.h>

#include "dwc3.h"
#include "dwc3-regs.h"
#include "dwc3-types.h"

#include <stdio.h>
#include <string.h>

#define EP0_LOCK(dwc) (&(dwc)->eps[EP0_OUT].lock)

static void dwc3_queue_setup_locked(dwc3_t* dwc) {
  io_buffer_cache_flush_invalidate(&dwc->ep0_buffer, 0, sizeof(usb_setup_t));
  dwc3_ep_start_transfer(dwc, EP0_OUT, TRB_TRBCTL_SETUP, io_buffer_phys(&dwc->ep0_buffer),
                         sizeof(usb_setup_t), false);
  dwc->ep0_state = EP0_STATE_SETUP;
}

zx_status_t dwc3_ep0_init(dwc3_t* dwc) {
  // fifo only needed for physical endpoint 0
  zx_status_t status = dwc3_ep_fifo_init(dwc, EP0_OUT);
  if (status != ZX_OK) {
    return status;
  }

  for (unsigned i = EP0_OUT; i <= EP0_IN; i++) {
    dwc3_endpoint_t* ep = &dwc->eps[i];
    ep->enabled = false;
    ep->max_packet_size = EP0_MAX_PACKET_SIZE;
    ep->type = USB_ENDPOINT_CONTROL;
    ep->interval = 0;
  }

  return ZX_OK;
}

void dwc3_ep0_reset(dwc3_t* dwc) {
  fbl::AutoLock lock(EP0_LOCK(dwc));

  dwc3_cmd_ep_end_transfer(dwc, EP0_OUT);
  dwc->ep0_state = EP0_STATE_NONE;
}

void dwc3_ep0_start(dwc3_t* dwc) {
  fbl::AutoLock lock(EP0_LOCK(dwc));

  dwc3_cmd_start_new_config(dwc, EP0_OUT, 0);
  dwc3_ep_set_config(dwc, EP0_OUT, true);
  dwc3_ep_set_config(dwc, EP0_IN, true);

  dwc3_queue_setup_locked(dwc);
}

static zx_status_t dwc3_handle_setup(dwc3_t* dwc, usb_setup_t* setup, void* buffer, size_t length,
                                     size_t* out_actual) {
  if (setup->bmRequestType == (USB_DIR_OUT | USB_TYPE_STANDARD | USB_RECIP_DEVICE)) {
    // handle some special setup requests in this driver
    switch (setup->bRequest) {
      case USB_REQ_SET_ADDRESS:
        zxlogf(TRACE, "SET_ADDRESS %d\n", setup->wValue);
        dwc3_set_address(dwc, setup->wValue);
        *out_actual = 0;
        return ZX_OK;
      case USB_REQ_SET_CONFIGURATION: {
        zxlogf(TRACE, "SET_CONFIGURATION %d\n", setup->wValue);
        dwc3_reset_configuration(dwc);
        dwc->configured = false;
        zx_status_t status =
            usb_dci_interface_control(&dwc->dci_intf, setup, nullptr, 0, nullptr, 0, out_actual);
        if (status == ZX_OK && setup->wValue) {
          dwc->configured = true;
          dwc3_start_eps(dwc);
        }
        return status;
      }
      default:
        // fall through to usb_dci_interface_control()
        break;
    }
  } else if (setup->bmRequestType == (USB_DIR_OUT | USB_TYPE_STANDARD | USB_RECIP_INTERFACE) &&
             setup->bRequest == USB_REQ_SET_INTERFACE) {
    zxlogf(TRACE, "SET_INTERFACE %d\n", setup->wValue);
    dwc3_reset_configuration(dwc);
    dwc->configured = false;
    zx_status_t status =
        usb_dci_interface_control(&dwc->dci_intf, setup, nullptr, 0, nullptr, 0, out_actual);
    if (status == ZX_OK) {
      dwc->configured = true;
      dwc3_start_eps(dwc);
    }
    return status;
  }

  if ((setup->bmRequestType & USB_DIR_MASK) == USB_DIR_IN) {
    return usb_dci_interface_control(&dwc->dci_intf, setup, nullptr, 0, buffer, length, out_actual);
  } else {
    return usb_dci_interface_control(&dwc->dci_intf, setup, buffer, length, nullptr, 0, out_actual);
  }
}

void dwc3_ep0_xfer_not_ready(dwc3_t* dwc, unsigned ep_num, unsigned stage) {
  fbl::AutoLock lock(EP0_LOCK(dwc));

  switch (dwc->ep0_state) {
    case EP0_STATE_SETUP:
      if (stage == DEPEVT_XFER_NOT_READY_STAGE_DATA ||
          stage == DEPEVT_XFER_NOT_READY_STAGE_STATUS) {
        // Stall if we receive xfer not ready data/status while waiting for setup to complete
        dwc3_cmd_ep_set_stall(dwc, EP0_OUT);
        dwc3_queue_setup_locked(dwc);
      }
      break;
    case EP0_STATE_DATA_OUT:
      if (ep_num == EP0_IN && stage == DEPEVT_XFER_NOT_READY_STAGE_DATA) {
        // end transfer and stall if we receive xfer not ready in the opposite direction
        dwc3_cmd_ep_end_transfer(dwc, EP0_OUT);
        dwc3_cmd_ep_set_stall(dwc, EP0_OUT);
        dwc3_queue_setup_locked(dwc);
      }
      break;
    case EP0_STATE_DATA_IN:
      if (ep_num == EP0_OUT && stage == DEPEVT_XFER_NOT_READY_STAGE_DATA) {
        // end transfer and stall if we receive xfer not ready in the opposite direction
        dwc3_cmd_ep_end_transfer(dwc, EP0_IN);
        dwc3_cmd_ep_set_stall(dwc, EP0_OUT);
        dwc3_queue_setup_locked(dwc);
      }
      break;
    case EP0_STATE_WAIT_NRDY_OUT:
      if (ep_num == EP0_OUT) {
        if (dwc->cur_setup.wLength > 0) {
          dwc3_ep_start_transfer(dwc, EP0_OUT, TRB_TRBCTL_STATUS_3, 0, 0, false);
        } else {
          dwc3_ep_start_transfer(dwc, EP0_OUT, TRB_TRBCTL_STATUS_2, 0, 0, false);
        }
        dwc->ep0_state = EP0_STATE_STATUS;
      }
      break;
    case EP0_STATE_WAIT_NRDY_IN:
      if (ep_num == EP0_IN) {
        if (dwc->cur_setup.wLength > 0) {
          dwc3_ep_start_transfer(dwc, EP0_IN, TRB_TRBCTL_STATUS_3, 0, 0, false);
        } else {
          dwc3_ep_start_transfer(dwc, EP0_IN, TRB_TRBCTL_STATUS_2, 0, 0, false);
        }
        dwc->ep0_state = EP0_STATE_STATUS;
      }
      break;
    default:
      zxlogf(ERROR, "dwc3_ep0_xfer_not_ready unhandled state %u\n", dwc->ep0_state);
      break;
  }
}

void dwc3_ep0_xfer_complete(dwc3_t* dwc, unsigned ep_num) {
  fbl::AutoLock lock(EP0_LOCK(dwc));

  switch (dwc->ep0_state) {
    case EP0_STATE_SETUP: {
      usb_setup_t* setup = &dwc->cur_setup;

      void* vaddr = io_buffer_virt(&dwc->ep0_buffer);
      zx_paddr_t paddr = io_buffer_phys(&dwc->ep0_buffer);
      memcpy(setup, vaddr, sizeof(*setup));

      zxlogf(TRACE, "got setup: type: 0x%02X req: %d value: %d index: %d length: %d\n",
             setup->bmRequestType, setup->bRequest, setup->wValue, setup->wIndex, setup->wLength);

      bool is_out = ((setup->bmRequestType & USB_DIR_MASK) == USB_DIR_OUT);
      if (setup->wLength > 0 && is_out) {
        // queue a read for the data phase
        io_buffer_cache_flush_invalidate(&dwc->ep0_buffer, 0, dwc->ep0_buffer.size);
        dwc3_ep_start_transfer(dwc, EP0_OUT, TRB_TRBCTL_CONTROL_DATA, paddr, dwc->ep0_buffer.size,
                               false);
        dwc->ep0_state = EP0_STATE_DATA_OUT;
      } else {
        size_t actual;
        zx_status_t status = dwc3_handle_setup(dwc, setup, vaddr, dwc->ep0_buffer.size, &actual);
        zxlogf(TRACE, "dwc3_handle_setup returned %d actual %zu\n", status, actual);
        if (status != ZX_OK) {
          dwc3_cmd_ep_set_stall(dwc, EP0_OUT);
          dwc3_queue_setup_locked(dwc);
          break;
        }

        if (setup->wLength > 0) {
          // queue a write for the data phase
          io_buffer_cache_flush(&dwc->ep0_buffer, 0, actual);
          dwc3_ep_start_transfer(dwc, EP0_IN, TRB_TRBCTL_CONTROL_DATA, paddr, actual, false);
          dwc->ep0_state = EP0_STATE_DATA_IN;
        } else {
          dwc->ep0_state = EP0_STATE_WAIT_NRDY_IN;
        }
      }
      break;
    }
    case EP0_STATE_DATA_OUT: {
      dwc3_endpoint_t* ep = &dwc->eps[ep_num];
      dwc3_trb_t trb;
      dwc_ep_read_trb(ep, ep->fifo.current, &trb);
      ep->fifo.current = nullptr;
      zx_off_t received = dwc->ep0_buffer.size - TRB_BUFSIZ(trb.status);

      // "actual" is unused for OUT transfers.
      size_t actual;
      zx_status_t status = dwc3_handle_setup(dwc, &dwc->cur_setup, io_buffer_virt(&dwc->ep0_buffer),
                                             received, &actual);
      if (status != ZX_OK) {
        dwc3_cmd_ep_set_stall(dwc, EP0_OUT);
        dwc3_queue_setup_locked(dwc);
        break;
      }
      dwc->ep0_state = EP0_STATE_WAIT_NRDY_IN;
      break;
    }
    case EP0_STATE_DATA_IN:
      dwc->ep0_state = EP0_STATE_WAIT_NRDY_OUT;
      break;
    case EP0_STATE_STATUS:
      dwc3_queue_setup_locked(dwc);
      break;
    default:
      break;
  }
}
